<?php
/**
*   Little minesweeper game
*   writte in PHP-Gtk2
*
*   @author Christian Weiske <cweiske@cweiske.de>
*/
class Game_Minesweeper extends GtkWindow
{
    protected $hboxTable;
    protected $lblMines;
    protected $btnRestart;
    protected $lblTime;

    protected $nFieldWidth  = 10;
    protected $nFieldHeight = 10;
    protected $nNumMines    = 10;
    protected $nMarkersLeft = 10;
    protected $nHiddenFields = -1;

    protected $arValues     = array();
    protected $arButtons    = array();
    protected $arRevealed   = array();
    protected $arMarked     = array();

    protected $nTime        = 0;
    protected $nTimeId      = null;

    protected $colExploded  = null;
    protected $colMarked    = null;
    protected $colDefault   = null;
    protected $colWon       = null;

    protected static $arSurroundings = array(
        array(-1,-1),array(-1,0),array(-1,1),
        array( 0,-1),            array( 0,1),
        array( 1,-1),array( 1,0),array( 1,1),
    );
    protected static $arDirectNeighbours = array(
        array(-1,0),array(0,-1),array(0,1),array(1,0)
    );



    public function __construct()
    {
        parent::__construct();
        $this->buildGui();
        $this->resetField();
        $this->set_default_size(400, 400);
        $this->show_all();
        $this->initColors();
        $this->updateStatus();
    }//public function __construct()



    protected function initColors()
    {
        $this->colExploded = GdkColor::parse('#F00');
        $this->colMarked   = GdkColor::parse('#FF0');
        $this->colWon      = GdkColor::parse('#0F0');
        $this->colDefault  = $this->arButtons[0][0]->get_style()->bg[Gtk::STATE_NORMAL];
    }//protected function initColors()



    protected function buildGui()
    {
        $this->set_title('Minesweeper');
        $this->connect_simple('destroy', array('gtk', 'main_quit'));

        $vboxMain = new GtkVBox();
        $hboxStatus = new GtkHBox();

        $this->lblStatus = new GtkLabel();
        $this->btnRestart = new GtkButton();
        $this->btnRestart->add(GtkImage::new_from_stock(Gtk::STOCK_REFRESH, Gtk::ICON_SIZE_SMALL_TOOLBAR));
        $this->btnRestart->connect_simple('clicked', array($this, 'resetField'));
        $this->btnRestart->connect_simple('clicked', array($this, 'show_all'));
        $this->lblTime   = new GtkLabel('00:00');
        $hboxStatus->pack_start($this->lblStatus);
        $hboxStatus->pack_start($this->btnRestart, false);
        $hboxStatus->pack_start($this->lblTime);

        $this->hboxTable = new GtkHBox();

        $vboxMain->pack_start($hboxStatus, false);
        $vboxMain->pack_start($this->hboxTable);

        $this->add($vboxMain);
    }//protected function buildGui()



    public function resetField()
    {
        $table = new GtkTable($this->nFieldWidth, $this->nFieldHeight, true);
        $table->set_row_spacings(0);
        $table->set_col_spacings(0);

        $this->arValues   = array();
        $this->arButtons  = array();
        $this->arRevealed = array();
        $this->arMarked   = array();
        //fill that with buttons
        for ($nX = 0; $nX < $this->nFieldWidth; $nX++) {
            $this->arValues[$nX]    = array_fill(0, $this->nFieldHeight, 0);
            $this->arRevealed[$nX]  = array_fill(0, $this->nFieldHeight, false);
            $this->arMarked[$nX]    = array_fill(0, $this->nFieldHeight, false);
            for ($nY = 0; $nY < $this->nFieldWidth; $nY++) {
                $this->arButtons[$nX][$nY] = new GtkButton(' ');
                $this->arButtons[$nX][$nY]->add_events(Gdk::BUTTON_RELEASE_MASK);
                $this->arButtons[$nX][$nY]->connect_simple_after('clicked', array($this, 'clickField'), $nX, $nY);
                $this->arButtons[$nX][$nY]->connect('button-release-event', array($this, 'releasedField'), $nX, $nY);
                $table->attach($this->arButtons[$nX][$nY], $nX, $nX + 1, $nY, $nY + 1);
            }
        }

        $this->setMines();
        $this->nMarkersLeft = $this->nNumMines;
        $this->nHiddenFields = $this->nFieldWidth * $this->nFieldHeight;

        $children = $this->hboxTable->get_children();
        if (count($children) > 0) {
            $this->hboxTable->remove($children[0]);
        }
        $this->hboxTable->add($table);
        $this->startClock();
        $this->updateStatus();
    }//public function resetField()



    /**
    *   Sets the mines randomly
    */
    protected function setMines()
    {
        $nMinesLeft = $this->nNumMines;
        do {
            $nX = rand(0, $this->nFieldWidth - 1);
            $nY = rand(0, $this->nFieldHeight - 1);
            if ($this->arValues[$nX][$nY] >= 0) {
                $this->arValues[$nX][$nY] = -9;
                $nMinesLeft--;
                //surroundings
                foreach (self::$arSurroundings as $arSurround) {
                    $nNewX = $nX + $arSurround[0];
                    $nNewY = $nY + $arSurround[1];
                    if ($nNewX >= 0 && $nNewY >= 0 && $nNewX < $this->nFieldWidth && $nNewY < $this->nFieldHeight) {
                        $this->arValues[$nNewX][$nNewY]++;
                    }
                }
            }
        } while ($nMinesLeft > 0);
    }//protected function setMines()



    protected function showHintAll()
    {
        for ($nX = 0; $nX < $this->nFieldWidth; $nX++) {
            for ($nY = 0; $nY < $this->nFieldWidth; $nY++) {
                $nValue = $this->arValues[$nX][$nY];
                if ($nValue != 0) {
                    $this->arButtons[$nX][$nY]->set_label($nValue);
                }
            }
        }
    }//protected function showHintAll()



    public function clickField($nX, $nY)
    {
        $nValue = $this->arValues[$nX][$nY];
        if ($nValue < 0) {
            //Mine
            $this->showAllMines();
            $this->looseGame();
        } else {
            //empty field or mines near -> show
            $this->clearFieldRecursive($nX, $nY);
            if ($this->nHiddenFields == $this->nNumMines) {
                $this->winGame();
            }
        }
    }//public function clickField($nX, $nY)



    public function releasedField($button, $event, $nX, $nY)
    {
        if ($event->state & Gdk::BUTTON1_MASK) {
            return;
        }
        if ($this->arMarked[$nX][$nY]) {
            $this->arMarked[$nX][$nY] = false;
            $this->nMarkersLeft++;
            $this->colorButton($nX, $nY, $this->colDefault);
            $this->updateStatus();
        } else if ($this->nMarkersLeft > 0) {
            $this->arMarked[$nX][$nY] = true;
            $this->nMarkersLeft--;
            $this->colorButton($nX, $nY, $this->colMarked);
            $this->updateStatus();
        }
    }//public function releasedField($button, $event, $nX, $nY)



    /**
    *   Clears the field (if within boundaries)
    *   and tries to do that with its neighbours
    */
    protected function clearFieldRecursive($nX, $nY)
    {
        if ($nX < 0 || $nY < 0 || $nX >= $this->nFieldWidth || $nY >= $this->nFieldHeight) {
            return;
        }
        if ($this->arRevealed[$nX][$nY]) {
            return;
        }
        $this->arRevealed[$nX][$nY] = true;
        $this->nHiddenFields--;

        if ($this->arValues[$nX][$nY] == 0) {
            //empty -> hide
            $this->arButtons[$nX][$nY]->hide();
            foreach (self::$arDirectNeighbours as $arNeighbour) {
                $this->clearFieldRecursive($nX + $arNeighbour[0], $nY + $arNeighbour[1]);
            }
        } else {
            //value should not be < 0, always > 0
            //if not, something is really wrong
            $this->arButtons[$nX][$nY]->set_label($this->arValues[$nX][$nY]);
            $this->arButtons[$nX][$nY]->set_sensitive(false);
        }
    }//protected function clearFieldRecursive($nX, $nY)



    /**
    *   Sets the background color of the button determined
    *   by $nX and $nY to the given color
    */
    protected function colorButton($nX, $nY, $color)
    {
        $style = $this->arButtons[$nX][$nY]->get_style()->copy();
        $style->base[Gtk::STATE_NORMAL] = $color;
        $style->bg[Gtk::STATE_NORMAL]   = $color;
        $style->bg[Gtk::STATE_PRELIGHT] = $color;
        $this->arButtons[$nX][$nY]->set_style($style);
    }//protected function colorButton($nX, $nY, $color)



    /**
    *   Colors all the mines in the field
    *   with the exploded colors
    */
    protected function showAllMines()
    {
        for ($nX = 0; $nX < $this->nFieldWidth; $nX++) {
            for ($nY = 0; $nY < $this->nFieldWidth; $nY++) {
                if ($this->arValues[$nX][$nY] < 0) {
                    $this->arButtons[$nX][$nY]->set_label('X');
                    $this->colorButton($nX, $nY, $this->colExploded);
                }
            }
        }
    }//protected function showAllMines()



    /**
    *   Shows the button and enables it (makes it sensitive)
    */
    protected function showEnableButton($nX, $nY) 
    {
        $this->arButtons[$nX][$nY]->show();
        $this->arButtons[$nX][$nY]->set_label(' ');
        $this->arButtons[$nX][$nY]->set_sensitive(true);
    }//protected function showEnableButton($nX, $nY) 



    /**
    *   Draws a border around the game field
    *   by coloring all the buttons
    */
    protected function colorEdge($color)
    {
        for ($nX = 0; $nX < $this->nFieldWidth; $nX++) {
            $this->colorButton($nX, 0, $color);
            $this->showEnableButton($nX, 0);
            $this->colorButton($nX, $this->nFieldHeight - 1, $color);
            $this->showEnableButton($nX, $this->nFieldHeight - 1);
        }
        for ($nY = 1; $nY < $this->nFieldHeight - 1; $nY++) {
            $this->colorButton(0, $nY, $color);
            $this->showEnableButton(0, $nY);
            $this->colorButton($this->nFieldWidth - 1, $nY, $color);
            $this->showEnableButton($this->nFieldWidth - 1, $nY);
        }
    }//protected function colorEdge($color)



    protected function updateStatus()
    {
        $this->lblStatus->set_label($this->nMarkersLeft . '/' . $this->nNumMines);
    }//protected function updateStatus()



    public function setTime()
    {
        $this->nTime++;
        $nSecs = $this->nTime % 60;
        $nMins = ($this->nTime - $nSecs) / 60;
        $this->lblTime->set_label($nMins . ':' . str_pad($nSecs, 2, '0', STR_PAD_LEFT));
        return true;
    }//public function setTime()



    protected function startClock()
    {
        $this->stopClock();
        $this->nTime = 0;
        $this->nTimeId = Gtk::timeout_add(1000, array($this, 'setTime'));
    }//protected function startClock()



    protected function stopClock()
    {
        if ($this->nTimeId !== null) {
            Gtk::timeout_remove($this->nTimeId);
            $this->nTimeId = null;
        }
    }//protected function stopClock()



    /**
    *   The game is over, but the user has lost
    */
    protected function looseGame()
    {
        $this->colorEdge($this->colExploded);
        $this->endGame(false);
    }//protected function looseGame()



    /**
    *   The game is over, the user has won
    */
    protected function winGame()
    {
        $this->colorEdge($this->colWon);
        $this->endGame(true);
    }//protected function winGame()



    protected function endGame($bWon)
    {
        $this->stopClock();
    }//protected function endGame($bWon)

}//class Game_Minesweeper extends GtkWindow
?>